﻿// $Id: Rule.cs 10 2008-02-29 15:58:02Z mcartoixa $

/* ***************************************************************************
 *                                                                           *
 * Copyright © 2007 NourY Solutions                                          *
 *                                                                           *
 * This file is part of DataRules.                                           *
 *                                                                           *
 * DataRules is free software; you can redistribute it and/or modify it      *
 * under the terms of the GNU Lesser General Public License as published     *
 * by the Free Software Foundation; either version 3 of the License, or      *
 * (at your option) any later version.                                       *
 *                                                                           *
 * DataRules is distributed in the hope that it will be useful, but          *
 * WITHOUT ANY WARRANTY; without even the implied warranty of                *
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU          *
 * Lesser General Public License for more details.                           *
 *                                                                           *
 * You should have received a copy of the GNU Lesser General Public          *
 * License along with this program.  If not, see                             *
 * <http://www.gnu.org/licenses/lgpl.html>.                                  *
 *                                                                           *
 *************************************************************************** */

/*! \file
 * \brief Definition of \c Salamanca.DataRules.Rule.
 *
 * \author $Author: mcartoixa $
 * \date $Date: 2008-02-29 16:58:02 +0100 (ven., 29 févr. 2008) $
 * \version $Revision: 10 $
 */

using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;

namespace Salamanca.DataRules
{



    ///////////////////////////////////////////////////////////////////////////
    ///
    /// <summary>Indicates the validation mode for a rule.</summary>
    ///
    ///////////////////////////////////////////////////////////////////////////

    public enum RuleValidationMode
    {
        /// <summary>This rule is always validated, whatever property name is specified for the validation.</summary>
        /// <remarks>This flag is valid for general rules only (which property name is set to <see cref="string.Empty">the empty string</see>).</remarks>
        Always,
        /// <summary>This rule is validated if its associated property name is involved in the validation.</summary>
        PropertyGeneral,
        /// <summary>This rule is only validated if its associated property name is specified for the validation.</summary>
        PropertySpecific
    }



    ///////////////////////////////////////////////////////////////////////////
    ///
    /// <summary>Interface implemented by a business rule.</summary>
    ///
    ///////////////////////////////////////////////////////////////////////////

    public interface IRule
    {

        /// <summary>Validates the rule on the specified target.</summary>
        /// <param name="target">The instance against which the rule is validated.</param>
        /// <returns><c>true</c> if the instance is valid for the rule, <c>false</c> if not.</returns>
        bool Validate(object target);

        /// <summary>The description of the rule.</summary>
        string Description
        {
            get;
        }

        /// <summary>The name of the property which the rule applies to.</summary>
        string PropertyName
        {
            get;
        }

        /// <summary>Gets a value indicating the validation mode for this rule.</summary>
        /// <value>Indicates the validation mode for this rule.</value>
        RuleValidationMode ValidationMode
        {
            get;
        }

    }



    ///////////////////////////////////////////////////////////////////////////
    ///
    /// <summary>Basic implementation of a business rule.</summary>
    ///
    ///////////////////////////////////////////////////////////////////////////

    public abstract class Rule:
        IRule
    {

        /// <summary>Constructor.</summary>
        private Rule()
        {
        }

        /// <summary>Constructor.</summary>
        /// <param name="description">The description of the rule.</param>
        /// <exception cref="ArgumentNullException">
        ///   <para><paramref name="description" /> is <c>null</c>.</para>
        /// </exception>
        protected Rule(string description):
            this(string.Empty, description)
        {
        }

        /// <summary>Constructor.</summary>
        /// <param name="propertyName">The name of the property which the rule applies to. For a general rule, this can be the empty string (<c>""</c>).</param>
        /// <param name="description">The description of the rule.</param>
        /// <exception cref="ArgumentNullException">
        ///   <para><paramref name="propertyName" /> is <c>null</c>.</para>
        ///   <para><paramref name="description" /> is <c>null</c>.</para>
        /// </exception>
        protected Rule(string propertyName, string description):
            this(propertyName, description, RuleValidationMode.PropertyGeneral)
        {
        }

        /// <summary>Constructor.</summary>
        /// <param name="propertyName">The name of the property which the rule applies to. For a general rule, this can be the empty string (<c>""</c>).</param>
        /// <param name="description">The description of the rule.</param>
        /// <param name="validationMode">Indicates the validation mode.
        /// <see cref="RuleValidationMode.Always" /> is valid only when <paramref name="propertyName" /> is set to the empty string (<c>""</c>).</param>
        /// <exception cref="ArgumentNullException">
        ///   <para><paramref name="propertyName" /> is <c>null</c>.</para>
        ///   <para><paramref name="description" /> is <c>null</c>.</para>
        /// </exception>
        /// <exception cref="ArgumentException">
        ///   <para><paramref name="validationMode" /> is set to <see cref="RuleValidationMode.Always" />
        /// and <paramref name="propertyName" /> is not set to the empty string (<c>""</c>).</para>
        /// </exception>
        protected Rule(string propertyName, string description, RuleValidationMode validationMode)
        {
            Debug.Assert(propertyName!=null);
            if (propertyName==null)
                throw new ArgumentNullException("propertyName");
            Debug.Assert(description!=null);
            if (description==null)
                throw new ArgumentNullException("description");
            Debug.Assert((validationMode!=RuleValidationMode.Always) || string.IsNullOrEmpty(propertyName));
            if ((validationMode==RuleValidationMode.Always) && !string.IsNullOrEmpty(propertyName))
                throw new ArgumentException(Resources.SR.CannotAlwaysValidateRuleException, "validationMode");

            _PropertyName=propertyName;
            _Description=description;
            _ValidationMode=validationMode;

#if (!NET_CF)
            if (Configuration.Switch.TraceInfo)
            {
                Trace.WriteLine(
                    string.Format(
                        CultureInfo.CurrentCulture,
                        "Created {0} rule",
                        GetType()
                    ),
                    Traces.GetCategory(Configuration.Switch)
                );
                if (Configuration.Switch.TraceVerbose)
                {
                    Trace.Indent();
                    Trace.WriteLine(
                        string.Format(
                            CultureInfo.CurrentCulture,
                            "Property name: {0}",
                            propertyName
                        )
                    );
                    Trace.WriteLine(
                        string.Format(
                            CultureInfo.CurrentCulture,
                            "Description: {0}",
                            description
                        )
                    );
                    Trace.WriteLine(
                        string.Format(
                            CultureInfo.CurrentCulture,
                            "ValidationMode: {0}",
                            validationMode
                        )
                    );
                    Trace.Unindent();
                }
            }
#endif
        }

        /// <summary>Validates the rule on the specified target.</summary>
        /// <param name="target">The instance against which the rule is validated.</param>
        /// <returns><c>true</c> if the instance is valid for the rule, <c>false</c> if not.</returns>
        public bool Validate(object target)
        {
            bool ret=DoValidate(target);

            Traces.TraceValidation(Configuration.Switch, this, ret);

            return ret;
        }

        /// <summary>Validates the rule on the specified target.</summary>
        /// <param name="target">The instance against which the rule is validated.</param>
        /// <returns><c>true</c> if the instance is valid for the rule, <c>false</c> if not.</returns>
        protected abstract bool DoValidate(object target);

        /// <summary>The description of the rule.</summary>
        public virtual string Description
        {
            get
            {
                return _Description;
            }
        }

        /// <summary>The name of the property which the rule applies to.</summary>
        public string PropertyName
        {
            get
            {
                return _PropertyName;
            }
        }

        /// <summary>Gets a value indicating the validation mode for this rule.</summary>
        /// <value>Indicates the validation mode for this rule.</value>
        public RuleValidationMode ValidationMode
        {
            get
            {
                return _ValidationMode;
            }
        }

        /// <summary>The description of the rule.</summary>
        private string _Description;
        /// <summary>The name of the property which the rule applies to.</summary>
        private string _PropertyName;
        /// <summary>Indicates the validation mode for this rule.</summary>
        private RuleValidationMode _ValidationMode;
    }



    ///////////////////////////////////////////////////////////////////////////
    ///
    /// <summary>Basic implementation of a generic business rule.</summary>
    ///
    ///////////////////////////////////////////////////////////////////////////

    public abstract class GenericRule<T>:
        Rule
    {
        /// <summary>Initializes a new instance of the <see cref="GenericRule{T}" /> class.</summary>
        /// <param name="propertyName">The name of the property. This cannot be an empty string (<c>""</c>).</param>
        /// <param name="description">The description of the rule.</param>
        /// <exception cref="ArgumentException">
        ///   <para><paramref name="propertyName" /> cannot be found on the specified <typeparamref name="T">type</typeparamref>.</para>
        ///   <para><paramref name="propertyName" /> is not a readable property.</para>
        ///   <para><paramref name="propertyName" /> is an indexer.</para>
        /// </exception>
        /// <exception cref="ArgumentNullException">
        ///   <para><paramref name="propertyName" /> is <c>null</c>.</para>
        ///   <para><paramref name="description" /> is <c>null</c>.</para>
        /// </exception>
        protected GenericRule(string propertyName, string description):
            this(propertyName, description, RuleValidationMode.PropertyGeneral)
        {
        }

        /// <summary>Constructor.</summary>
        /// <param name="propertyName">The name of the property. This cannot be an empty string (<c>""</c>).</param>
        /// <param name="description">The description of the rule.</param>
        /// <param name="validationMode">Indicates the validation mode.
        /// <see cref="RuleValidationMode.Always" /> is valid only when <paramref name="propertyName" /> is set to the empty string (<c>""</c>).</param>
        /// <exception cref="ArgumentException">
        ///   <para><paramref name="propertyName" /> cannot be found on the specified <typeparamref name="T">type</typeparamref>.</para>
        ///   <para><paramref name="propertyName" /> is not a readable property.</para>
        ///   <para><paramref name="propertyName" /> is an indexer.</para>
        ///   <para><paramref name="validationMode" /> is set to <see cref="RuleValidationMode.Always" />
        /// and <paramref name="propertyName" /> is not set to the empty string (<c>""</c>).</para>
        /// </exception>
        /// <exception cref="ArgumentNullException">
        ///   <para><paramref name="propertyName" /> is <c>null</c>.</para>
        ///   <para><paramref name="description" /> is <c>null</c>.</para>
        /// </exception>
        protected GenericRule(string propertyName, string description, RuleValidationMode validationMode):
            base(propertyName, description, validationMode)
        {
            if (!string.IsNullOrEmpty(propertyName))
            {
                _Property=typeof(T).GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy);

                // The property has to exist
                Debug.Assert(_Property!=null);
                if (_Property==null)
                    throw new ArgumentException(
                        string.Format(
                            CultureInfo.CurrentCulture,
                            Resources.SR.CannotFindPropertyException,
                            propertyName,
                            typeof(T)
                        ),
                        "propertyName"
                    );

                // It must be readable
                Debug.Assert(_Property.CanRead);
                if (!_Property.CanRead)
                    throw new ArgumentException(
                        string.Format(
                            CultureInfo.CurrentCulture,
                            Resources.SR.PropertyNotReadableException,
                            propertyName
                        ),
                        "propertyName"
                    );

                // It must not be an indexer
                Debug.Assert(_Property.GetIndexParameters().Length==0);
                if (_Property.GetIndexParameters().Length>0)
                    throw new ArgumentException(Resources.SR.CannotValidateIndexerException, "propertyName");
            }
        }

        /// <summary>The property associated to the rule.</summary>
        protected PropertyInfo Property
        {
            get
            {
                return _Property;
            }
        }

        /// <summary>The property associated to the rule.</summary>
        private PropertyInfo _Property;
    }



    ///////////////////////////////////////////////////////////////////////////
    ///
    /// <summary>Arguments for the <see cref="E:Rules.CreatingRules" /> event.</summary>
    ///
    ///////////////////////////////////////////////////////////////////////////

    public sealed class CreatingRulesEventArgs:
        EventArgs
    {

        /// <summary>Initializes a new instance of the <see cref="CreatingRulesEventArgs" /> class.</summary>
        /// <param name="targetType">The type for which rules are being created.</param>
        public CreatingRulesEventArgs(Type targetType):
            base()
        {
            _TargetType=targetType;
        }

        /// <summary>Gets the rules created.</summary>
        /// <value>The rules created.</value>
        public IList<IRule> Rules
        {
            get
            {
                return _Rules;
            }
        }

        /// <summary>Gets the type for which rules are being created.</summary>
        /// <value>The type for which rules are being created.</value>
        public Type TargetType
        {
            get
            {
                return _TargetType;
            }
        }

        /// <summary>The rules created.</summary>
        private IList<IRule> _Rules=new List<IRule>();
        /// <summary>The type for which rules are being created.</summary>
        private Type _TargetType;

    }



    ///////////////////////////////////////////////////////////////////////////
    ///
    /// <summary>Container class for common rules methods.</summary>
    ///
    ///////////////////////////////////////////////////////////////////////////

    public static class Rules
    {

        /// <summary>Creates a message based on the description of rules in the specified collection. </summary>
        /// <param name="rules">The collection of rules.</param>
        /// <returns>The message based on the description of the specified rules.</returns>
#if (NET_1_0 || NET_1_1 || NET_CF_1_0)
        public static string GetMessage(IEnumerable rules)
#else
        public static string GetMessage(IEnumerable<IRule> rules)
#endif
        {
            StringBuilder ret=new StringBuilder();

            foreach (IRule r in rules)
#if (!NET_CF)
                ret.AppendLine(r.Description);
#else
                ret.AppendFormat(CultureInfo.CurrentCulture, "{0}\r\n", r.Description);
#endif

            return ret.ToString().Trim();
        }

        /// <summary>Validates the specified target against the specified collection of rules.</summary>
        /// <remarks>This method can be used to help implement <see cref="IValidatable" /> on a custom type.</remarks>
        /// <param name="target">The instance to validate.</param>
        /// <param name="propertyName">The name of the property to validate. This can be an empty string (<c>""</c>) or <c>nulll</c> to validate the target against all the specified rules.</param>
        /// <param name="rules">The collection of rules.</param>
        /// <returns><c>true</c> if the <paramref name="target" /> is valid.</returns>
        public static bool Validate(object target, string propertyName, IEnumerable<IRule> rules)
        {
            foreach (IRule r in rules)
            {
                bool validate=false;

                switch (r.ValidationMode)
                {
                case RuleValidationMode.Always:
                    validate=true;
                    break;
                case RuleValidationMode.PropertyGeneral:
                    validate=(string.IsNullOrEmpty(propertyName) || (string.Compare(propertyName, r.PropertyName, false, CultureInfo.InvariantCulture)==0));
                    break;
                case RuleValidationMode.PropertySpecific:
                    validate=(!string.IsNullOrEmpty(propertyName) && (string.Compare(propertyName, r.PropertyName, false, CultureInfo.InvariantCulture)==0));
                    break;
                }

                if (validate)
                {
                    if (!r.Validate(target))
                        return false;
                }
            }

            return true;
        }

        /// <summary>Gets the broken rules for the specified target in the specified collection of rules.</summary>
        /// <remarks>This method can be used to help implement <see cref="IValidatable" /> on a custom type.</remarks>
        /// <param name="target">The instance to check.</param>
        /// <param name="propertyName">The name of the property to check. This can be an empty string (<c>""</c>) or <c>null</c> to validate the target against all the specified rules.</param>
        /// <param name="rules">The collection of rules.</param>
        /// <returns>The collection of all the broken rules for the specified <paramref name="target" />.</returns>
        public static ReadOnlyCollection<IRule> GetBrokenRules(object target, string propertyName, IEnumerable<IRule> rules)
        {
            List<IRule> ret=new List<IRule>();

            foreach (IRule r in rules)
            {
                bool validate=false;

                switch (r.ValidationMode)
                {
                case RuleValidationMode.Always:
                    validate=true;
                    break;
                case RuleValidationMode.PropertyGeneral:
                    validate=(string.IsNullOrEmpty(propertyName) || (string.Compare(propertyName, r.PropertyName, false, CultureInfo.InvariantCulture)==0));
                    break;
                case RuleValidationMode.PropertySpecific:
                    validate=(!string.IsNullOrEmpty(propertyName) && (string.Compare(propertyName, r.PropertyName, false, CultureInfo.InvariantCulture)==0));
                    break;
                }

                if (validate)
                {
                    if (!r.Validate(target))
                        ret.Add(r);
                }
            }

            return new ReadOnlyCollection<IRule>(ret);
        }

        /// <summary>Creates the list of rules for the specified type, based on the attributes specified on its public properties.</summary>
        /// <param name="targetType">The type to create the rules for.</param>
        /// <returns>The list of rules.</returns>
        [SuppressMessage("Microsoft.Design", "CA1002:DoNotExposeGenericLists")]
        public static IList<IRule> CreateRules(Type targetType)
        {
            List<IRule> ret=new List<IRule>();

            foreach (PropertyInfo pi in targetType.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy))
                foreach (Attribute a in Attribute.GetCustomAttributes(pi))
                {
                    RuleAttribute ra=a as RuleAttribute;
                    if (ra!=null)
                    {
                        IRule r=ra.GetRule(targetType, pi.Name);
                        if (r!=null)
                            ret.Add(r);
                    }
                }

            EventHandler<CreatingRulesEventArgs> eh=CreatingRules;
            if (eh!=null)
            {
                CreatingRulesEventArgs args=new CreatingRulesEventArgs(targetType);
                eh(targetType, args);
                ret.AddRange(args.Rules);
            }

            return ret;
        }

        /// <summary>Event triggered when rules are <see cref="CreateRules(Type)">being created</see> for a type.</summary>
        public static event EventHandler<CreatingRulesEventArgs> CreatingRules;

    }

}
